The IO::Socket API


إسترجاع retrieve delivered مسلم ،يسلم presses يضغط Process عملية

تحدثنا في السابق انشاء المقابس بإستخدام الدوال الداخلية لمقابس بيركيلي التي تنعكس في الواقع لإستدعاءات مكتبة السي ، من الجيد استخدامها لتحكم في كيفية انشاء المقبس والبروتوكول المستخدمة...الخ ، لكننا واجهنا شيء من الصعوبه واسهل طريقة في البيرل هي عن طريق وحـدة IO::Socket تعتبر هذة الوحـدة البرمجية تكملة لإمتدادت الكائنات الموجهه لعائلة IO:: المستندة على الصنف الأساس (class base ) وحـدة IO::Handle ، ومن الجيد لتثبيت العلل minor bug في الأمثلة الأصلية ( ترك التصعيب ) ، وفي هذا الفصل نأخذ مقدمة لهذة API ثم نمضي قدماً الى بعض اساسيات تطبيقات TCP ،و فيما يلي نعيد كتابة الاكواد السابقة لفصل 17 و 18 لكن بإستخدام IO::Socket .


A Daytime Client

في المثال السابق لخدمة daytime في فصل 17 يعمل العميل على تثبيت الإتصال الخارج إلى خادم daytime ويقراء سطر من المحتوى ثم الخروج .

العديد من خادمات الإنترنت كخدمة daytime تنهي السطور بالCRLF بدلاً من وضع LF فقط كما تعمل البيرل ، وقبل القراءة من daytime نضع حرف end-of-line إلى CRLF ، وغير ذلك سيحتوي السطر الذي قرأنا على CR غريب في النهاية .

#!/usr/bin/perl
# file: time_of_day_tcp2.pl
# Time of day client using IO::Socket

use strict;
use IO::Socket qw(:DEFAULT :crlf);

my $host = shift || 'localhost';
$/ = CRLF;

my $socket = IO::Socket::INET->new("$host:daytime") 
    or die "Can't connect to daytime service at $host: $!\n";

chomp(my $time = $socket->getline);
print $time,"\n";

الشرح:

تشغيل وحدة IO::Socket وإستيراد ثوابت ":crlf" وايضاً الثوابت الإفتراضية :DEFAULT التي تعاد تصديرها من Socket ، ثم إسترداد إسم المضيف البعيد من نأفذة الاوامر .

ثم يأتي وضع فاصلة نهاية السطر لقراءة السطور من daytime server بوضع المتغير $/ إلى CRLF ، ولإحظ بأن هذا المتغير الخاص عام أي ان التأثير على جميع مقابض الملفات وليس فقط المقبس ، ثم يلية إنشاء المقبس بإنشاء كائن جديد IO::Socket عن طريق االإستدعاء بالطريقة new لكائن IO::Socket::INET مع تحديد عنوان المستقبل في شكل $host:service سنرى لاحقاً طرق اخرى لتحديد عنوان المستقبل .

ثم قراءة سطر الوقت time of day من الخادم بإستدعى getline() وحذف CRLF من النهاية مع chomp() ومن ثم طباعة السطر الى STDOUT .


نحصل على خرج هذة البرنامج:


Output

% time_of_day_tcp.pl wuarchive.wustl.edu
Tue Aug 15 07:39:49 2000





TCP Echo Client

نشاهد نسخة اخرى من العميل خدمة echo بإستخدام الكائنات الموجهه

#!/usr/bin/perl
# file: tcp_echo_cli2.pl
# TCP echo client using IO::Socket

# usage: tcp_echo_cli2.pl [host] [port]

use strict;
use IO::Socket;
my ($bytes_out,$bytes_in) = (0,0);

my $host = shift || 'localhost';
my $port = shift || 'echo';

my $socket = IO::Socket::INET->new("$host:$port") or die $@;

while (defined(my $msg_out = STDIN->getline)) {
    print $socket $msg_out;
    my $msg_in = <$socket>;
    print $msg_in;

    $bytes_out += length($msg_out);
    $bytes_in  += length($msg_in);
}

$socket->close or warn $@;
print STDERR "bytes_sent = $bytes_out, bytes_received = $bytes_in\n";

الشرح :

بتشغيل السكربت تشحن IO::Socket وتهيئة الثوابت والمتغيرات العامة ، ثم إنشاء المقبس بإستدعاء الطريقة IO::Socket::INET->new() بإستخدام وسيط $host:$port ، تعيد في النجاح كائن المقبس متصل إلى المضيف البعيد ، وفي حلقة التكرار في كل مرة تستدعى getline() على مقبض الملف STDIN لإسترجاع سطر مدخل من المستخدم ، ثم إرسال هذا السطر الى المضيف البعيد بإستخدام print() على المقبس ، ومن ثم قراءة الرد من الخادم بواسطة المعامل <> وطباعة الرد الى الخرج القياسي ، وتسجيل عدد البيانات المرسلة والمستلمة .


من الملاحظ في السكربت أستخدام طريقة الكائن الموجهه getline() مع STDIN التي جلبت IO::Socket بشحن داخلي من وحـدة IO::Handle ، كذلك نستطيع أستخدام اي من طرق IO::Handle لحدوث هذا التأثير على كأفة مقابض الملفات .

على خلاف daytime client ليس من الداعي إستخدم نوع نهاية السطر لخدمة echo وذلك بسبب معوادة echo client لإظهارة لنا بالضبط مالذي سنرسلة .

نجد أيضاً لان نحتاج الى وضع نمط autoflush على كائن IO::Socket ، لكؤن autoflush ممكنة في الإفتراضي على جميع المقابس التي تنشأ وليس من الضرورة عمل ذلك بنفسنا في نسخة بيرل 5.00503 ومافوق .



الترتيب الهرمي IO::Handle

تتدرج في الصورة الترتيب الهرمي للصنف IO::Handle ، وتنجز شجرة عائلة IO::Handle في صيغة الكائنات الموجهه لمختلف طرق الخرج والدخل I/O methods ، وتتفرع منها مباشرتاً صنف IO::Socket التي تستقر لمقابس بركيلي ، لدى IO::Socket فرعين ايضاً مشتقة وهم IO::Socket::INET و IO::Socket::UNIX ، يعرف الاول سلوكاً لتعامل لمقابس في مجال الإنترنت ويعرف الاخر كالسلوك الملائم للمقابس المحلية مثل AF_UNIX ( المعرف كذلك AF_LOCAL ).

لانستطيع إنشاء كائن IO::Socket مباشرتاً ومن المهم تحديد المقبس سواءاً كائن IO::Socket::INET أو كائن IO::Socket::UNIX ،سوف نستخدم الصنف الفرعي IO::Socket::INET في هذا الفصل و في معظم بقية الكتاب ، وأصبحت النسخ المستقبلية للمقابس مدعمة للعنوانة المجال الحديثة IPV6 .

كما رينا في السابق بأن IO::Handle تضمن في أغلب الاصناف كال IO::File المذكورة في السابق و IO::Pipe التي تنجز تفاعل كائنات موجهه لإستدعى البيرل pipe() ، ويتفرع من IO::Socket::INET الصنف Net::Cmd التي تمثل الأب للجميع عائلة الوحـدات الاضافية third-party modules التي تنجز التفاعل الى للإوامر الموجهه command-oriented خدماءت الشبكة المتضمنة بذلك بروتوكول FTP و Post Office Protocol وسنرى هذة الوحدات في الفصل التالي .

بالرغم من عدم تفرع مباشر من IO::Handle والوحـدات الاخرى في فضاء أسم IO::* بضمن IO::Dir للمعالجة المجلدات ، و IO::Select لوضع إختبار للمقبض الملفات لإستعدادهم لإتأدية الخرج والدخل I/O ، وIO::Seekable لإداء وصول عشوائي في الملف على القرص ، سنتقدم IO::Select في الفصل حيث تضمن لخوادم الشبكة بإستعمال I/O multiplexing.

إنشاء كائنات IO::Socket::INET :

انشاء كائنات IO::Socket::INET مماثل لعائلة IO::Handle والكائنات الموجهه مع الطريقة new() :

$sock = IO::Socket::INET->new('wuarchive.wustl.edu:daytime');

ثم يستعمل هذا الكائن في وظائف الدخل والخرج I/O المتعلقة للمقبس ، الى جأنب أستخدام طرق IO::Handle كطرق القراءة والكتابة وإدارة شروط الخطأ بألاضافة يضيف الصنف المتفرع منه IO::Socket::INET طرق المقبس الموجهه مثل accept(), connect(), bind(), and sockopt().

كماهو الحال للكائنات الموجهه يمكن أستخدام أحد الطرقتين لإستدعى الطرق ولديك الخيار في ذلك

$sock = IO::Socket->new(
Domain => ’INET’, @args);

Or
$sock = IO::Socket::INET->new(@args);
Or
$sock = new IO::Socket::INET(@args);

# أستخدام الكائن مع أستدعى الطريقة 
$sock->print('Here comes the data.');

# استخدام الكائن كمقبض ملف 
print $sock 'Ready or not, here it comes.';


$socket = IO::Socket::INET->new (@args);

طريقة new() لإنشاء كائن الصنف IO::Socket::INET التي تعيد الكائن أو في الخطأ تعيد undef ، ويحتوي المتغير الخاص $! على خطأ النظام ويحتوي المتغير الخاص $@ على أكثر وصف للخطأ المولد بواسطة الموديل نفسها .

يمكن أستعمال المضيف ورقم المنفذ معاً مفصول بينهم الرمز ":" ، ينشاء IO::Socket::INET مقبس TCP ويبحث عن المضيف ورقم الخدمة ومن ثم يقوم ببناء تركيب sockaddr_in صحيح ، ثم يحاول الإتصال connect() الى المضيف البعيد واي شكل من الأشكال التالية صحيحة .

wuarchive.wustl.edu:echo wuarchive.wustl.edu:7 128.252.120.8:echo 128.252.120.8:7

بألاضافة يمكن تحديد الخدمة اما بالإسم أو برقم المنفذ ، ونستطيع جمع الاثنين معاً التي تحاول IO::Socket::INET لإيجاد إسم الخدمة أولاً وفي حالتة أن لم يكن صحيح تعاود إلى إستخدم رقم منفذ hard-coded ، الصيغة هي hostname:service(port) مثلاً للإتصال الى خدمة echo لل wuarchive :

my $echo = IO::Socket::INET->new('wuarchive.wustl.edu:echo(7)')
            or die "Can't connect: $!\n";

نستطيع من الطريقة new() لتركيب المقابس المناسب لوصول الإتصالات القادمة ،و incoming connections, UDP communications, broadcasting ..الى آخرة ، ولها عموماً وسطاء في شكل مطابق مثل هذا :

my $echo = IO::Socket::INET->new(PeerAddr => 'wuarchive.wustl.edu',
                                 PeerPort => 'echo(7)',
                                 Type   => SOCK_STREAM,
                                 Proto  => 'tcp')
            or die "Can't connect: $!\n";

كما في السابق يفصل وسطاء الهاش اما بالرمز "," أو الرمز المرادف " => " ويوضع كل زواج في الشكل name/argument على كل سطر جديد ،وتلخص قائمة الوسطاء في الجدول أسفل لعبورهم الى IO::Socket::INET .



الوسيط الوصف القيمة
PeerAddr عنوان المضيف البعيد أو الند الذي نرغب الاتصال به <HOSTNAME OR ADDRESS>[:<PORT>]
PeerHost مشابه للـ PeerAddr
PeerPort منفذ المضيف البعيد أو الخدمة <SERVICE NAME OR NUMBER>
LocalAddr ربط عنوان المضيف المحلي <HOSTNAME OR ADDRESS>[:PORT]
LocalHost مشابه للـ LocalAddr
LocalPort ربط منفذ المضيف المحلي <SERVICE NAME OR PORT NUMBER>
Proto اسم البروتوكول أو الرقم <PROTOCOL NAME OR NUMBER>
Type نوع المقبس SOCK_STREAM | SOCK_DGRAM | ...
Listen حجم الطابور للإستماع <INTEGER>
Reuse تسمح للاستعمال مره اخرى للعنوان المحلي بوضع SO_REUSEADDR قبل عمل الربط binding <BOOLEAN>
Timeout قيمة Timeout (مده الاتصال قبل ان ينقطع بالثانية) <INTEGER>
MultiHomed محاوله جميع العنوانين على multihomed من المضفين ، فالعميل قد يتصل الى الخادم بالعديد من عناوين IP بإفتراض انة لدية اكثر من عنوان (multihomed) فهو مفيد خصوصاً عندما العميل لايعرف أياُ من عناوين الخادم لكي يرتبط بة . <BOOLEAN>
Arguments IO::Socket::INET


PeerAddr و PeerHost

كلا الوسطين متشابهتان تستخدم في تحديد المضيف لاتصال به ، وعند امررها الى IO::Socket::INET تحاول الإتصال connect() إلى المضيف المشار ، نضع للقيمة أما بإسم المضيف أو عنوان IP أو نسطيع تضمين المنفذ دون الحاجة الى وسيط PeerPort .


PeerPort

تشير الى رقم المنفذ الذي نرغب الاتصال بة ،يمكن أن يكون برقم منفذ عددي أو أسم خدمة رمزي أو حتي جمع الاثنين معاً مثل "ftp(22)" ، يستخدم هذا الوسيط متى ماكان غير مضمن في أسم المضيف .


LocalAddr و LocalHost و LocalPort

تستخدم الثلاثة الوسطاء في البرامج التي تعمل كالخوادم التي نرغب لقبول الاتصالات القادمة ،الوسيطين LocalAddr و LocalHost متشابهان لتحديد عنوان IP لوصلة شبكة المضيف المحلي ، فيما يحدد LocalPort رقم المنفذ المحلي ، وفي حالتة مشاهدة الكائن IO::Socket::INET أي من تلك الوسطاء سيقوم ببناء عنوان محلي ويحاول ربطه إلى الدالة bind() إليه .

تحدد وصلة الشبكة كعنوانIP في شكل dotted-quad مقروء أو كإسم مضيف DNS hostname أو كعنوان IP مضغوط ، ورقم المنفذ يمكن ان يعطى رقم منفذ أو أسم خدمة أو مجتمعة معا ً "service(port)" مثلاً "127.0.0.1:http(80)" فعند اجتماعهم ستأخذ الموديل IO::Socket::INET رقم المنفذ من الـ LocalAddr وتتجاهل القيمة المعطى لوسيط LocalPort ، و أذا حددنا المنافذ في LocalPort ولم نحددة في LocalAddr فسوف تربطة الموديل إلى INADDR_ANY wildcard الذي يمنح المقبس لقبول الاتصالات من اي وصلة شبكة للمضيف ، وهو مانريده غالباً.


Listen

لبرامج السيل الموجهه التي نرغب لقبول الإتصال القادمة يجب ايضاًَ من تحديد الوسطاء Listen و Reuse لاحتمال المعاودة ،فيعطى Listen بحجم طابور الإستماع وفي تضمين هذا الوسيط فسوف تستدعى IO::Socket الدالة listen() بعد أنشاء المقبس الجديد بإستعمال الوسيط كطول طابورة ، وهذا الوسيط الزامي اذا أردنا أستدعى accept() لاحقاً .


Reuse

تخبر القيمة true الموديل IO::Socket::INET لوضع الخيار SO_REUSEADDR على المقبس الجديد ، وهذا مفيد لخوادم الإتصال الموجهه connection-oriented servers التي تحتاج لكي تقوم باعادة تشغيلها من وقت الى اخر ، وبدون هذا الخيار يجب على الخادم الإنتظار بعض الدقائق بين الخروج واعادة التشغيل لتفادءي أخطاء "address in use" أثناء الإستدعى للربط bind() .


Proto و Type

لتحديد بروتوكول ونوع المقبس ، قد ياتي البروتوكول بشكل رمزي كالـ "tcp" أو عددي بإستخدام القيمة المعادة بواسطة الدالة getprotobyname() ، أما النوع يجب ان يكون واحد من ثوابت SOCK_* مثلاً SOCK_STREAM.....الخ، وأذا لم تزود أحد أو كلتا هذة الخيارات فسوف تخمن IO::Socket::INET القيم الصحيحة من السياق ، فمثلاً اذا Type غير موجود تعطي IO::Socket:: INET النوع الصحيح من البروتوكول ، واذا Proto غير موجود لكن إسم الخدمة معطى للمنفذ فسوف تحاول IO::Socket::INET إيجاد البروتوكول الصحيح لكي يستخدم من إسم الخدمة أو تلجى IO::Socket::INET كملاذ أخير الى أستخدام الإفتراضي "tcp" .


Timeout

لوضع قيمة لزمن الخروج Timeout في الثواني ويستخدم فقط مع الوظائف operations المؤكده ، حالياً يستخدم الـ timeouts للاستداعى الداخلي إلى connect() وفي الطريقة accept() ، وهو مفيداً لمنع برنامج العميل من التعليق الغير محدود في حالتة كان المضيف البعيد لايمكن الوصول الية unreachable.


MultiHomed

هذا الخيار مفيد في بعض الحالات الغير موحدة لبرنامج العميل الذي نرغب للإتصال إلى المضيف لدية العديد من عناوين IP ولانعرف أي عنوان IP نستخدم، فأن وضع الخيار إلى القيمة true سوف تستعمل الطريقة new() الدالة gethostbyname() للنظر إلى جميع عنوانين IP لإسم المضيف المحدد سابقاً من قبلPeerAddr ، ومن ثم تحاول الإتصال بكل عنوانين IP للمضيف متتابعه حتى ينجح واحد منهم .



للتلخيص :

عملاء TCP الذين يرغبون لصنع إتصالات خارجة يجب إستدعاء new() مع وسيط Proto إلى tcp ، وأما عنوان الند PeerAddr يضمن رقم المنفذ أو استخدام الزوج PeerAddr/PeerPort مثلاً :

my $sock = IO::Socket::INET->new(Proto  => 'tcp',
                                 PeerAddr => 'www.yahoo.com',
                                 PeerPort => 'http(80)');

خادم TCP الذي نرغب منه لقبول الإتصالات القادمة يجب ان يستدعى new() مع Proto إلى " tcp " ، وأن يشير وسيط LocalPort إلى المنفذ الذين نرغب لربطة معه ، وأن يشير وسيط Listen إلى طول الطابور الإستماع المطلوب :

my $listen = IO::Socket::INET->new(Proto   => 'tcp',
                                   LocalPort => 2007,
                                   Listen  => 128);

نناقش لاحقاً في فصل UDP Servers تحتاج تطبيقات UDP لتزويد فقط وسيط Proto إلى " udp " أو وسيط Type إلى SOCK_DGRAM ، والتعبير نفسة لكلا الخوادم والعملاء :

my $udp = IO::Socket::INET->new(Proto => 'udp');


طرق كائن IO::Socket :

عندما ينشاء المقبس نستطيع أستخدام الكائنات مع مختلف دوال الخرج والدخل ومنها print(), read(), <>, sysread() الى أخرة ، والى جأنب أستخدام طرق IO::Handle يضيف IO::Socket::INET طرق المقبس الموجهه مثل accept(), connect(), bind(), and sockopt() بتحديد الطرق إلى الكائن :


$connected_socket = $listen_socket->accept() ($connected_socket,$remote_addr) = $listen_socket->accept()

هذة الطريقة تؤدي نفس مهمة دالة accept() وتسعمل فقط على المقبس المستمع ، تسترجع accept() الإتصال القادم التالي من الطابور ، وتعيد جلسة مقبس متصل لبداء الجلسة والتواصل مع المضيف البعيد ، ويرث المقبس الجديد كل خواص والده ماعدا أنة متصل .

عندما نستدعي الدالة في سياق scalar تعيد الدالة accept() المقبس المتصل ،عندما تستدعى في سياق القائمة تعيد الدالة عنصرين فالاول هو المقبس المتصل والاخر عنوان مضغوط للمضيف البعيد ، ونسطيع ايضاً من أستعادة هذا العنوان في وقت لاحق بإستدعى المقبس المتصل بطريقة peername() .


$return_val = $sock->connect ($dest_addr) $return_val = $sock->bind ($my_addr) $return_val = $sock->listen ($max_queue)

هذة الطرق الثلاث المتعلقة بروتوكول TCP تستخدم نادراً لانهم يستدعون بشكل آلي عند أستدعاء new()، وعلى اي حال اذا رغبت في تضمينهم في الكود يدوياً يمكنك ذلك بإنشاء مقبس جديد TCP بدون تأدية اي من الوسطاء PeerAddr أو Listen كالتالي :

$sock = IO::Socket::INET->new(Proto=>'tcp'); $dest_addr = sockaddr_in(...) # etc. $sock->connect($dest_addr);


$return_val = $sock->connect ($port, $host) $return_val = $sock->bind ($port, $host)

في الظروف المناسبة بطريقة معدلة لكلتا الدالتين connect() and bind() عن طريق وسيطين ، التي تأخذ منفذ مفكوك ضغطة وعناوين المضيف بدلاً من العنوان المضغوط ، ويمكن ان يعطى عنوان المضيف في شكل dotted-IP أو كشكل نصي بأسم المضيف .


$return_val = $socket->shutdown($how)

كما في إستدعاء shutdown() بطريقة أجبارية في إغلاق المقبس التي تغلق المقبس حتى في وجود نسخ مفتوحة في الطفل المتفرع ، وكما في $how للسيطرة على أطفاء جزء من الإتصال ذو الاتجايين بوضع القيم (0،1،2)كما في السابق .


$my_addr = $sock->sockname() $her_addr = $sock->peername()

طرق sockname() and peername() لفت بشكل بسيط لدول الموجهة المكافئة ، وكما مع دوال الدالية يعيدون عناوين المقبس مضغوط التي يجب فتح ضغطهم بإستخدام sockaddr_in() .


$result = $sock->sockport() $result = $sock->peerport() $result = $sock->sockaddr() result = $sock->peeraddr()

هذة الطرق الاربع تودئ الوظائف براحة التي تفك ضغط القيم المعادة بواسطة sockname() and peername() ، وتعيد sockport() and peerport() أرقام المنفذ لطرفين المحلي والبعيد local and remote endpoints للمقبس على التوالي ، تعيد sockaddr(), and peeraddr() عنوان IP لطرفين المحلي والبعيد للمقبس كتراكيبات ثنائية مناسبة لمرورها إلى gethostbyaddr() ، لتحويل الناتج إلى شكل dotted-quad ، التى مازالت تحتاج لتشيد بتضمين inet_ntoa() .


$my_name = $sock->sockhost() $her_name = $sock->peerhost()

تاتي هذة الطرق بخطوة ابعد للإمام ، وتعيد عناوين IP المحلي والبعيد في شكل dotted-quad كامل ("aa.bb.cc.dd") ، واذا رغبت إلى إستعادة أسم DNS للند فسوف يرجع الى شكل dotted-quad في حال فشل DNS وهنا التعبير :

$peer = gethostbyaddr($sock->peeraddr,AF_INET) || $sock->peerhost;


$result = $sock->connected()

تعيد الطريقة connected() القيمة true اذا المقبس متصل إلى المضيف البعيد وغير ذلك فهي تعيد false ، فهي تعمل بواسطة إستدعى peername() .


$protocol = $sock->protocol() $type = $sock->socktype() $domain = $sock->sockdomain()

تعيد الثلاث الطرق معلومات أساسية حول المقبس من ضمنها بروتوكولها العددي ، ونوعة ، ومجالة ، يمكن لهذة الطرق إن تستخدم للحصول على خواص لكائن المقبس ، ولايمكن ان تستخدم لتغير طبيعة الكائن المنشاء .


$value = $sock->sockopt($option [,$value])

يمكن ان تسخدم الطريقة sockopt() للحصول أو/و وضع get and/or set خيار المقبس ، وهي واجهة أمامية لكتا الدالتين getsockopt() and setsockopt().فتسدعى الطريقة على وسيط عددي وحيد لتسترجع الطريقة sockopt() القيمة الحالية للخيار ، وتستدعى على الخيار والقيمة الجديدة لتضع الطريقة sockopt() الخيار الى القيمة المشارة وتعيد الناتج للإشارة الى النجاح أو الفشل ، وليس هناك الحاجة الى تحديد level الخيار ،كما عملنا مع getsockopt() بسبب أن وسيط SOL_SOCKET مفترضة.

على خلاف دالة getsockopt() الداخلية ، تقوم طريقة الكائن آلياً بتحويل الوسيط المضغوط المعاد بواسطة إستدعاء النظام التحتي system call إلى العدد صحيح integer ، لذلك لانحتاج الى فك ضغط قيم الخيار المعادة بواسطة sockopt() ، كما ناقشنا في وقت سابق أغلب الإستثناء الاكثر تكراراً الى هذا هو الخيار SO_LINGER الذي يشغل على تركيب الإطالة linger structureبثمانية بايت 8-byte كوسيط له .


$val = timeout([$timeout])

تحصل أو تضع قيمة timeout التي يستخدمها IO::Socket للطرق connect(), and accept() الخاصة بتلك الموديل ، تستدعى مع وسيط عددي عندما نرغب لوضع قيمة timeout وتعيد القيمة التي أعددت حالياً ، وغير ذلك تعيد القيمة الحالية المعددة ،حالياً لاتستخدم قيمة timeout للإستدعاءات التي ترسل أو تستلم بيانات لكن عن طريق إستخدام اجراء eval{} المناقشة سابقاً في فصل العمليات والإشارات لكي يستعمل لإنجاز ذلك النتيجة (الغرض ).


$bytes = $sock->send ($data [, $flags ,$destination]) $address = $sock-> recv ($buffer,$length [,$flags])

تلك الطرق واجهات أمامية front ends للدوال send() and recv() وستناقش في التفصيل في أتصالات UDP في فصل UDP Servers

الأثر الجانبي في الإهتمام في حالتة تضمين timeout التي تضع الموديل IO::Socket::INET الtimeout تجعل من إستدعاء connect() and accept() عرضة للمقاطعة بإلاشارات ، وهذا يسمح لقائد الإشارة للمقاطعة بشكل أفضل للبرنامج الذي يعلق في الإنتظار على connect() or accept() ، وسنرى ذلك في المثال في القسم القادم .

أمثلة أكثر عملية :

سنرى في التالي بعض الأمثلة الإضافية تصور IO::Socket API ، أولاً سنعيد كتابة نسخة محسنة لIO::Socket API من الفصل TCP Protocol السابق ، والثاني برنامج بسيط Web client .

Reverse Echo Server Revisited

في المثال reverse echo server المشاهد أسفل سنعيد كتابة البرنامج السابق بالأضافة إلى ان يكون أكثر روعة من النسخة السابقة (وأسهل لإتتبع في رأيي ) ونتطرق الى عدة تحسينات في هذة النسخة بضمن قائد الإشارة الخطر مع قائد الذي يضع ببساطة العلم والعائد ، وهذا يتفادى المشاكل التي تنجم عن جعل أستدعاءات الخرج والدخل I/O ضمن القائد ومشاكل exit() على أرصفة الونيدوز ، والتحسين الاخر جعل الخادم يحلل resolves الاسماء للإتصالات القادمة وطباعة أسم المضيف البعيد ورقم المنفذ الى الخطأ القياسي ، واخيراً سنلاحظ أتفاقية الإنترنت بأن يستعمل الخوادم CRLF المتسلسلة لنهايات الاسطر ، وهذا يعني بوضع $/ إلى CRLF وتذيل CRLF إلى نهاية جميع كتابتنا ، وقائمة الاكواد التالية للخادم المحسن .

#!/usr/bin/perl
# file: tcp_echo_serv2.pl
# The reverse echo server, using IO::Socket

# usage: tcp_echo_serv2.pl [port]

use strict;
use IO::Socket qw(:DEFAULT :crlf);
use constant MY_ECHO_PORT => 2007;
$/ = CRLF;
my ($bytes_out,$bytes_in) = (0,0);

my $quit = 0;
$SIG{INT} = sub { $quit++ }; 

my $port     = shift || MY_ECHO_PORT;

my $sock = IO::Socket::INET->new( Listen    => 20, 
                                  LocalPort => $port,
                                  Timeout   => 60*60,
                                  Reuse     => 1) 
  or die "Can't create listening socket: $!\n";

warn "waiting for incoming connections on port $port...\n";
while (!$quit) {
  next unless my $session = $sock->accept;

  my $peer = gethostbyaddr($session->peeraddr,AF_INET) || $session->peerhost;
  my $port = $session->peerport;
  warn "Connection from [$peer,$port]\n";

  while (<$session>) {
    $bytes_in  += length($_);       
    chomp;
    my $msg_out = (scalar reverse $_) . CRLF;
    print $session $msg_out;
    $bytes_out += length($msg_out);
  }
  warn "Connection from [$peer,$port] finished\n";
  close $session;
}

print STDERR "bytes_sent = $bytes_out, bytes_received = $bytes_in\n";
close $sock;

الشرح :

بتشغيل السكربت تفحص strict الصيغة وشحن الموديل IO::Socket بإستيراد الثوابت الإفتراضية وثوابت المتعلقة بالسطر الجديد عن طريق التأج :DEFAULT and :crlf ، ثم تعريف ثابت المنفذ المحلي وضع قيم أولية لمتغرات المرسلة والمستلمة وأيضا وضع المتغير العام $/ إلى CRLF لتوافق مع أتفاقيات الشبكة .

تنزيل قائد الإشارة INT لكي يطفئ shut down الخادم بشكل أفضل عندما يضغط المستخدم مفتاح الإعتراض ، والقائد المحسن ببساطة يضع العلم المسمى $quit إلى القيمة true .

ثم أنشاء كائن المقبس عن طريق إستعادة المنفذ من سطر الاوامر أو أستخدام الأفتراضي أذا لم يحدد وفي كلتا الحالتين نفترض كؤن الثابت hard-coded ، ثم إستدعاء IO::Socket::INET->new() مع الوسطاء التي تسبب إنشاء المقبس المستمع يعين إلى bound to المنفذ المحلي المحدد ، وتوضع الوسطاء الاخرى الخيار SO_REUSEADDR إلى true ، وقيمة لزمن الخروج timeout بساعة (التي هي 60*60 ثانية ) لقبول accept() .

يجعل وسيط Timeout لكل إستدعاء إلى الطريقة accept() تعيد undef في حالتة قدوم أتصال قادم لم يستلم بضمن ذلك الزمن المحدد ،على اية حال تحفيز هذة الميزة ليست في مصلحتنا الخاصة لكنها في الحقيقة تغير سلوك الطريقة لكي لاتعيد تشغيل restarted بشكل آلي بعد أن عورض من قبل الإشارة ، وهذا يمنحنا إلى مقاطعة الخادم مع مفتاح ^C بدون إزعاج للف accept() في بلوك eval{} كما في السابق في قسم Timing Out Slow System Calls .

وفي حلقة التكرار الرئيسية بعد أتمام طباعة رسائلة الحالة ، ندخل في الحلقة التي تستمر حتى قائد أشارة INT يضع لمتغير $quit إلى القيمة 1 ، وكل مرة عبر التكرار إستدعاء طريقة المقبس accept() ، فأن أستكملت الطريقة accept() بدون أن يعترض بالإشارة أو بخروج timing out على يملك فسوف يعاد كائن مقبس متصل جديد الذي سنخزن في المتغير المسمى $session وغير ذلك تعيد accept() غير معرف وفي هذة الحالة سيعود إلى بداءية حلقة التكرار ، فذلك يعطينا الفرصة للإختبار سواءاً قائد الإعتراض وضع المتغير $quit إلى القيمة 1 .

ثم الحصول على أسم المضيف البعيد والمنفذ يمكن ذلك باستدعى الطريقة peeraddr() للمقبس المتصل لنحصل على عنوان IP مضغوط للنهاية الاخرى من الإتصال ، ومحالة إلى ترجمتة إلى أسم المضيف بإستخدام gethostbyaddr() ، فأن ذلك فشل تعيد undef ونستدعي الطريقة peerhost() لتعطينا عنوان المضيف البعيد في شكل dotted-quad ، ومن ثم الحصول على رقم منفذ المضيف البعيد بإستخدام peerport() وطباعتة العنوان ورقم المنفذ إلى الخطأ القياسي .

ثم معالجة الإتصال بقراءة السطور من المقبس المتصل ، وعكسهم reverse ، وطباعتهم إلى المقبس ، ثم تتبع عدد البايتات المرسلة والمستلمة بينما نعمل ذلك ، والتغير الوحيد من المثال السابق قمنا بإنهاء كل سطر مع CRLF ، فعندما يستكمل المضيف البعيد من العمل نحصل على EOF عندما نحاول إلى القراءة من المقبس المتصل ، ثم طباعة التحذير خارجاً على الخرج القياسي وإغلاق المقبس المتصل ثم العودة إلى الحلقة الرئيسية لقبول accept() مرة أخرى .


عند تشغيل السكربت يعمل مثل النسخة السابقة لكن رسائل الحالة تعطى بإسماء المضيف بدلاً من عناوين IP .


Output

% tcp_echo_serv2.pl
waiting for incoming connections on port 2007...
Connection from [localhost,2895]
Connection from [localhost,2895] finished
Connection from [formaggio.cshl.org,12833]
Connection from [formaggio.cshl.org,12833] finished
^C
bytes_sent = 50, bytes_received = 50




A tiny Web Client

بالرغم ان الويب يدعم الكثير من البروتوكولات الا ان البروتوكول الرئيسي لخوادم الويب Hypertext Transfer Protocol (HTTP) الذي يجمع بين القوة والمرؤنة ببساطتة ويستخدم لإنتقال صفحات الويب عبر الإنترنت ويستخدم لجميع تواصلات الويب بين المتصفحات وخوادم الويب ، الاصدار الحالية HTTP 1.1 المعرف في RFC 2616 على موقع اتحاد الشبكة العنكوبتية العالمية W3C

فيما يلي سنكتب عميل ويب صغير له الاسم web_fetch.pl بحيث يمكنه قراءة Universal Resource Locator (URL) من سطر الاوامر ثم يعرب parses الى العنوان URL الصحيح ثم تشغيل الطلب ومن ثم طباعة رد خادم الويب إلى الخرج القياسي .ان لهذا السكربت أهمية خاصة لأنة يرجع رد خأم "raw response"من خادم الويب بدون ان يعالج processing فكثيراً يستخدم في تنقيح بعض أسكربتات CGI (Common Gateway Interface) التي إساءت التصرف misbehaving ومع إنواع أخرى لمحتويات الدينامكية لخادم الويب Web-server .

يتركز بروتوكول HTTP على الطلبيات والردود ويتم التفاعل بين عميل وخادم الويب على الشكل التالي :

يقوم العميل أولاً بفحص المدخل لتقديق من قواعد اللغة عن طريق البحث عن الكلمات المقبوله لهذه اللغه وهذا مايعرف بالإعراب parsing URL بحيث يعطي التنسيق العام ل URL. و كمايلي التنسيق العام HTTP URLs:

http://hostname:port/path/to/document#fragment

كما نرى يتكون URL من سلسلة أحرف إبجدية تمثل مكان أحد المواقع(أو العناوين) على الانترنت .نجد ان جميع HTTP URLs يتقدمها http:// في المقدمة (في معظم المتصفحات إذا لم تحدد بروتوكلاً سيعتبر أن البروتوكول هو HTTP ايضاً يوجد بروتوكلات تواصلات اخرى مثل بروتوكول ftp ، file ) ويلية أسم المضيف(مثل www.yahoo.com )ورقم المنفذ الذي خادم الويب يستمع علية مفصول بينهما علامة ":" ، من الممكن حذف العلامة ورقم المنفذ حيث يفترض الخادم القياسي تلقائياً المنفذ 80 كفتراضي ، ويعرف المسار path إلى الوثيقة أو الصفحة المعينة المطلوبة (أنة تنسيق مماثلة لليونكس في إتفاقيات مسار الملف )، يلية العلامة "#" مع اسم الجزء fragment الذي يشيران إلى قسم فرعي في الوثيقة يلتزم على متصفح الويب التحرك scroll إليها .

بعدما يقوم العميل بإعراب مكونات URL إلى أسم المضيف ورقم المنفذ (ان وجد)مجتمعة معاً والمسار ايضاً وتجاهل fragment name ، وبعد الإتصال إلى خادم معين بإستخدام مقبس TCP لابد من إرسال الطلب HTTP request بهذا الشكل :

GET /path/to/document HTTP/1.0 CRLF CRLF

يشمل طريقة الطلب "GET" يليه مسافة واحدة ، ثم المسار المعين منسوخ حرفياً من URL يلية أيضاً مسافة أخرى ، يلية رقم نسخة البروتوكول HTTP/1.0 ، ثم زوج من CRLF .فقط الطريقة get تسترجع محتوى صفحة html لكن استخدام الطريقة post في الغالب لتعبية في نماذج الويب web-forms .

أغلب الطلبات تستخدم HTTP للوثائق الموجودة ولكن بعض الطلبات لتنفيذ برنامج مع خرج معاد كوثيقة اي GET جلب الوثيقة POST تنفيذ الوثيقة بإستخدام البيانات في الجسم HEAD جلب رأس الوثيقة فقط PUT تخزين الوثيقة الجديدة على الخادم DELETE حذف الوثيقة من الخادم . الجدول التالي أغلب طرق HTTP Request المشتركة:


بعدما نقوم بإرسال الطلب سيتظر العميل حتي يتم الرد من قبل الخادم ، يبدو ان الرد المثالي مثل :

HTTP/1.1 200 OK Date: Wed, 01 Mar 2000 17:00:41 GMT Server: Apache/1.3.6 (UNIX) Last-Modified: Mon, 31 Jan 2000 04:28:15 GMT Connection: close Content-Type: text/html <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> <html> <head> <title> Presto Home Page </title> </head> <body> <h1>Welcome to Presto</h1> ...

نجد أن الرد مقسم إلى جزءين وهما header الذي يحتوي على معلومات حول الوثيقة المعادة ، والوثيقة المطلوبة نفسها ، وتفصل بين الجزءين بواسطة سطر فارغة المشكلة بإثنين أزواج CRLF السابقة . سننقب لاحقاُ الى تركيب رودود HTTP بأكثر تفصيل في فصل Web Clients ونناقش مكتبة LWP ، وفي فصل Nonblocking I/O نناقش أكثر تطويراً بأكثر حنكة sophisticated Web client قادراً على إسترجاع العديد من الؤثائق بشكل آنى simultaneously ، والقضية الوحيدة التي تدعو للقلق حول ذلك هي بينما يضمن بروتوكول HTTP الheader لتكون سطور من النصوص المقروءة وكل سطر منهم ينتهي بزوج CRLF ، ويمكن ان تأخذ الوثيقة نفسها أي شكل أو صيغة ، بشكل خاص يجب علينا أن نستحضر prepared لإستلام البيانات الثنائية مثل محتويات ملف GIF أو MP3 ، ونرى في مثال السكربت web_fetch.pl .

#!/usr/bin/perl
# file: web_fetch.pl
# Simple web page fetcher

use strict;
use IO::Socket qw(:DEFAULT :crlf);
$/ = CRLF . CRLF;
my $data;

my $url = shift or die "Usage: web_fetch.pl <URL>\n";

my ($host,$path) = $url=~m!^http://([^/]+)(/[^\#]*)! 
  or die "Invalid URL.\n";

my $socket = IO::Socket::INET->new(PeerAddr => $host, PeerPort => 'http(80)')
  or die "Can't connect: $!";

print $socket "GET $path HTTP/1.0",CRLF,CRLF;

my $header = <$socket>;    # read the header
$header =~ s/$CRLF/\n/g;   # replace CRLF with logical newline
print $header;

print $data while read($socket,$data,1024) > 0;

الشرح :

كمافي السابق تشغيل فحص الصيغة strict وشحن الموديل IO::Socket بإستيراد ثوابت DEFAULT والمعتلق بالسطر الجديد ، وكما سبق يمكن التعامل مع CRLF نجد في هذة الحالة وضع $/ ليكون زوج من CRLF متسلسل ، ولاحقاً عندما نستدعى معامل <> يقرأ header كاملاً(أسفل down through ) ينهيها بزوج CRLF .

ثم يلي الكود قسم التعريب(Parse URL) عن طريق قراءة URL المطلوب من سطر الاوامر وتعريبه parse بواسطة مطابقة النمط pattern match وإعادة أسم المضيف المطابق .

ثم فتح مقبس يتصل إلى خادم الويب البعيد ، ان أحتوى URL على رقم المنفذ فسوف يضمنة في أسم المضيف بإمرارة إلى PeerAddr ويتجاهل وسيط PeerPort ، وغير ذلك يحدد PeerPort بأن يجب ان يرتبط إلى الخدمة القياسية "http" على المنفذ 80 .

بإرسال الطلب بكتابة إلى المقبس ، بواسطة إرسال HTTP request إلى الخادم بأستخدام الصيغة المشارة سابقاً ، ثم قراءة وطباعة header أولاً قرنا سطر موجهه line-oriented من المقبس بإستخدام <> ، ولان المتغير $/ يوضع الى زوج من CRLF متسلسل ، هذة القراءة تمسك grabs كامل header كاملاً (أعلىup through )السطر الفارغ ، ثم طباعة header ولكن لكون لانرغب في CRs الغير صالحة لتشويش الخرج لذلك علينا أولاً إستبدال جميع حوادث $CRLF مع السطر الجديد المنطقي ("\n", الذي يقدر حرف السطر الجديد للرصيف الحالي مهما أختلاف اتفاقية الرصيف) .

في الاخير قراءة وطباعة الوثيقة ، نستكمل قراءة byte-oriented بإستدعى الدالة read() في الحلقة ولكل مرة قراءة 1024 بايت وطباعتهم مباشرتاً مع print() ، والخروج متى قرأنا EOF وإعادة الصفر .

وهنا نشاهد خرج web_fetch.pl أن رغبنا لجلب الصفحة الرئيسية لموقع www.cshl.org :


Output

% web_fetch.pl http://www.cshl.org/
HTTP/1.1 200 OK
Server: Netscape-Enterprise/3.5.1C
Date: Wed, 16 Aug 2000 00:46:12 GMT
Content-type: text/html
Last-modified: Fri, 05 May 2000 13:19:29 GMT
Content-length: 5962
Accept-ranges: bytes
Connection: close

<HTML>
<HEAD>
<TITLE>Cold Spring Harbor Laboratory</TITLE>

<META NAME="GENERATOR" CONTENT="Adobe PageMill 2.0 Mac"> <META
Name="keywords" Content="DNA, genes, genetics, genome, genome 
sequencing, molecular biology, biological science, cell biology, 
James D. Watson, Jim Watson, plant genetics, plant biology, 
bioinformatics, neuroscience, neurobiology, cancer, drosophila, 
Arabidopsis, double-helix, oncogenesis, Cold Spring Harbor 
Laboratory, CSHL">
...


يبدو السكربت مكتمل لجلب صفحة الويب مع 15 سطر فقط الأ انة يمكن للعميل عن طريق أستخدام موديل LWP عمل الشئ نفسة في سطر واحد فقط التي سنراها لاحقاً في فصل Web Clients .



الأداء والأسلوب

بالرغم من تبسيط IO::Socket API البرمجة من ناحية الجهد والتطوير وصيانة الكود الا انة لدية بعض العوائق مع التفاعل الدوال الموجهه الداخلية ، فأن كانت الذاكرة متسعملة memory usage فهي قضية ويجب أدرك بأن الموديل IO::Socket تضأف إلى ذاكرة عملية البيرل "footprint" بكمية significant تقارب 800 K في نظامي الينكس جهاز أنتل وأكثر من ذلك تقريباً الضعف على نظام Solaris ، ايضاً التعامل مع الكائنات الموجهه API تبطىء شحن البرنامج بعض الشيء ، فالبرامج التي تستخدم IO::Socket على الأجهزة المحمولة القديمة تأخذ حوالي نصف الوقت (أو نصف ثانية ) أطول لشحن تلك التي تسخدم التفاعل مع المقبس بإستخدام الدوال الموجهه function-oriented ، ولكن لحسن الحظ سرعة تنفيذ البرنامج بإستخدام IO::Socket لايختلف كثيراً من سرعة التفاعل الكلاسيكي classical بل بشكل غير ملحوظ لدى الحاسب ،وعموماً تحدد برامج الشبكة بسرعة الشبكة بدلاً من سرعة processing .

على الرغم من هذا لدى العديد من طرق IO::Socket المغلفة بإغلفة رقيقة لإستدعاءات النظام المقابلة ولاتضيف وظائف significant اي ليست significant ، فالأفضل إستخدام IO::Socket كمقابض ملفات واضحة plain filehandles بدلاً من إستخدام صيغة object-oriented على سبيل المثال بدلاً من كتابة :

 $socket->syswrite("A man, a plan, a canal, panama!");

سنكتب

syswrite ($socket,"A man, a plan, a canal, panama!");

لكلتا الصيغتين نفس التأثير في إستدعى الطريقة لكن الاخير يتفادئ overhead بتاعة أي لايتجاوز الاضافي له.

للطرق التي تحسن في أستدعاء الدالة لاتتردد في أستخدامها فمثلاً الافضل أستخدام الطريقة accept() المحسنة على الدالة الداخلية بسبب كونها تعيد كائن IO::Socket متصل بدلاً من مقبض الملف واضح plain filehandle ، أيضا الطريقة لها صيغة التي تعمل البيرل أكثر مثل الدالة الداخلية .



العملاء المتلاقون (Concurrent Clients)

في هذا القسم نقدم مقدمة لواحدة من أهم القضايا الرئيسية لفصول Developing TCP Client/Server Systems ومشاكل التلاقي concurrency .

ثرثرة العميل (Gab1 Client) :

لنكتب عميل بسيط الذي يستخدم للمحادثات التفاعلية مع خوادم السطر الموجهه line-oriented servers ، يتصل هذا البرنامج إلى مضيف ومنفذ معين ،ويتصرف ببساطة كقناة conduit مباشرة بين الجهاز البعيد والمستخدم ، فأي شيء يرسلها المستخدم عن طريق الكتابة إلى المضيف البعيد ،وإي شيء يستلمها العميل من المضيف البعيد تردد إلى الخرج القياسي .

يمكن إستخدام هذا العميل لتحدث مع echo servers من هذا الفصل أو لتحدث مع line-oriented servers أخر من على الانترنت ، والامثلة المشتركة بضمن FTP, SMTP, and POP3 servers.

سنطلق على العميل بإسم gab1.pl لكؤنة الاول في المجموعة كعميل و تضمينة بسيط —لكن غير صحيح—يبدو الكود كما في التالي

#!/usr/bin/perl
# file: gab1.pl
# An incorrect implementation of a gab client

# warning: this doesn't really work

use strict;
use IO::Socket qw(:DEFAULT :crlf);

my $host = shift or die "Usage: gab1.pl host [port]\n";
my $port = shift || 'echo';

my $socket = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $port)
  or die "Can't connect: $!";

my ($from_server,$from_user);

LOOP:
while (1) {
  {  # localize change to $/
    local $/ = CRLF;
    last LOOP unless $from_server = <$socket>;
    chomp $from_server;
  }
  print $from_server,"\n";

  last unless $from_user = <>;
  chomp($from_user);
  print $socket $from_user,CRLF;
}

كما سبق شحن الموديل وإستراجع المضيف البعيد المطلوب ورقم المنفذ من نأفذة الاوامر ومن ثم إنشاء المقبس بإستخدام IO::Socket::INET->new() أن فشلت تموت العملية die مع رسالة الخطأ .

في دخال التكرار تقراء في كل مرة سطر واحد من المقبس وتبطعة الى الخرج القياسي ، ثم نقراء سطر من مدخل المستخدم وطباعتة إلى المقبس .

بسبب إستخدام الخادم البعيد أزواج CRLF إلى نهاية أسطرة ، لكن أتفاقية المستخدم الخاصة به لسطر الجديد سنحتاج باستمرار من وضع واعادة وضع $/ ، واسهل طريقة لعمل ذلك هي وضع الكود الذي يقراء السطر من المقبس في بلوك صغير ، وموضعتة المتغير مع local حتي تكون القيمة الحالية محفوظة على مدخل entry لبلوك ،ويعاد تخزين على(عند) الخروج ، وبضمن البلوك وضعنا $/ إلى CRLF .

واذا حصلنا على EOF سواءاً من المستخدم أو الخادم سنخرخ من حلقة التكرار بإستدعى last .



في بداية الأمر يبدو السكربت بسيط للعمل لتؤضيح مثلاً لنعد جلسة مع خادم FTP ، فأول شيء نرى عند الإتصال مع الخادم رسائلة الترحيب الإعلانية (message code 220) نكتب الأمر USER لإدخال أسم مستخدم FTP مع أعطئة بإلاسم "anonymous" سنحصل بعد ذلك على الشكر acknowledgment ، ومن ثم نزود كلمة المرور مع PASS سنحصل على شكر acknowledgment أخرى ، كل شيء يبدو الا الان يسير على مايرام.


Output

% gab1.pl phage.cshl.org ftp
220 phage.cshl.org FTP server ready.
USER anonymous
331 Guest login ok, send your complete e-mail address as password.
PASS jdoe@nowhere.com
230 Guest login ok, access restrictions apply.

لسوء الحظ لاتدوم الإشياء طويلاً ، ولندخل شيء اخر مثلاً الامر HELP لطباعة المساعدة لإيعازات أوامر FTP ، نرى بانة لم ينجح بحصولنا على السطر الاول من الخرج المتوقع ، ومن ثم السكربت يتوقف stops وينتظر لنا لكتابة الامر التالي ، نكتب مرة اخرى HELP نحصل على السطر الثاني لخرج من امر HELP الاول ، لنكتب QUIT نحصل على السطر الثالث من الامر HELP .


HELP
214-The following commands are recognized (* =>'s unimplemented).
HELP
USER     PORT    STOR    MSAM*    RNTO    NLST    MKD   CDUP
QUIT
PASS     PASV    APPE    MRSQ*    ABOR    SITE    XMKD  XCUP
QUIT
ACCT*    TYPE    MLFL*   MRCP*    DELE    SYST    RMD   STOU
QUIT
...

بشكل واضح قد خرج أو ظهر أمرgotten out السكربت من التزامن synch وكما هو مكتوب في السكربت يمكنة التعامل مع حالة فقط الذي فية سطر واحد مدخل من المستخدم ينتج في سطر واحد لخرج من الخادم ، وليس له طريقة لتعامل مع خرج متعدد السطور multiline output ، وهو لايستطيع ان يلحقة بالرد للإمر HELP .

ماذا لو غيرنات السطر الذي يقرأ من الخادم إلى شيء كهذا ؟

while ($from_server = <$socket>) {
  chomp $from_server;
  print $from_server,"\n";
}

للإسف هذا يجعل المسألة اسوأ ، يعلق السكربت حالياًَ بعد ان يقرأ السطر الاول من الخادم ، في حالة FTP server يتنظر لنا لإرسال الاوامر ولكن السكربت لسطر أخر من الخادم وحتى أنة لايسألنا لحد الأن لندخل (تعرف هذة الحالة deadlock ) .

بشكل صريح لاتوجد اي شيء لإعادة ترتيب طلب القراءة والكتابة تصلح هذة المشكلة سواءاً اما الخروج get out من التزامن أو نحصل على الإنتظار الدائم deadlock .



ثرثرة العميل (Gab2 Client)

ماسنتاج هو لعمل فصل لعملية القراءة من المضيف البعيد من عملية القراءة من المقبس ، وفي واقع الامر سنحتاج لعزل المهام في الإثنين المتلاقية (أو العمليان المتلاقية ) لكن جميع العمليات المستقلة في الكود لاتستمر التكتل blocking بعضهم البعض الطريقة التي يتم فيها التضمين الساذج عمل العميل الثرثرة .

على أنظمة اليونكس والونيدوز الطريقة الأسهل لإنجاز هذة المهمة بإستخدام الأمر fork() لإنشاء نسختين من لبرنامج ، ستكون عملية الأب مسؤولة عن البيانات النسخة من الدخل القياسي إلى المضيف البعيد ، بينما يكون الطفل مسؤول عن تدفق البيانات في الإتجاه الأخر ، هذا شيء جيد لكن هناك بعض الحالات وتحل بأكثر تعيقداً التي تتفادئ إستدعى fork() تناقش لاحقاً في Multiplexed Operations لاحقاً في فصل Multiplexed Applications .

كما يظهر جزء بسيط simple part من السكربت يتصل إلى الخادم وتفرع الطفل ثم لكل عملية نسخة من البيانات عبر الشبكة ، الجزء الصعب (hard part الافضل )في مزامنة العمليتين بينما ينقل التفيذ بين العمليات حتى تخرج quit كلايهما عندما الجلسة تعمل وغير ذلك هناك فرصة للعملية من الإستمرار بإلإشتغال بعد ان تكون الاخر قد خرجت exited .

وهناك سيناريوان لكي ننهي الإتصال ، في الطريقة الأولة يبتداء الخادم البعيد بتحضر العملية بإغلاق نهاية مقبسة ، في هذة الحالة تسلم عملية الطفل EOF عندما تحاول في المرة القادمة لقراءة من الخادم وتستدعى exit() ، وبطريقة ما يجب عمل signal إلى الأب بأن ذلك قد تم أكتمالة ، والسيناريو الثاني تكتشف عملية الأب EOF عندما تقرأ من STDIN ويجب أن نخبر الطفل بأن الجلسة قد تمت .

على نظام اليونكس هناك طريقة للإطفال لتشير signal إلى الأب بأنهم قد خروجو ، ترسل الإشارة CHLD آلياً إلى الأب حينما واحد من عملياتة الفرعية قد ماتت died ( أو سواء تأخذ متوقفة أو مستأنفة stopped or resumed ، سناقش ذلك بأكثر تفصيل في فصل Forking Servers and the inetd Daemon ) ، لكي تكتشف عملية الأب بأن الخادم البعيد قد أغلاق أتصالة بمجرد تنزيل CHLD handler الذي يستدعى exit() ، وعندما تكتشف عملية الطفل بأن الخادم أغلاق الأتصال سيخرج exit الطفل وتولد إشارة CHLD ، ثم قائد إشارة يخرج exits عملية الأب أيضاً.

السيناريو الثاني الذي فية يغلق المستخدم STDIN معقد قليلاً واسهل طريقة هو ان يقوم الأب فقط إلى قتل kill() أطفالة بعد أغلاق الدخل القياسي ،وعلى اي حال هناك مشكلة من هذا العمل بسبب المستخدم فحسب قد إغلاق الدخل القياسي وهذا لايعني بأن الخادم إنتهاء من إلارسال الخرج العائد الينا ، وأذا قتلنا الطفل قبل يستلم البيانات من الخادم ومعالجة جميع المعلومات معلقة pending من الخادم فقد نفقد بعض المعلومات .

الطريقة الأوضح لعمل ذلك العمل مشاهدة في الصورة أسفل ،عندما علمية الأب تحصل على EOF من الدخل القياسي فسوف يغلق نهايتة من المقبس ، بذلك يرسل إلى الخادم شرط end-of-file ، عندما يكتشف الخادم الEOF يقوم بإغلاق نهايتة من الإتصال ، بذلك تمــد propagating الEOF عادئدةً إلى عملية الطفل ، فتخرج exit عملية الطفل مولدةً إشارة CHLD يلتقيها الأب ثم يخرج exit نفسة .



Closing a connection in a forked client

الشيء الجميل من ذلك بأن الطفل لايرى EOF حتى بعد أن نهي معالجة processing أي بيانات الخادم المطوبرة ، وهذا يضمن بعدم أي فقد للبيانات وفي الإضافة يعمل المخطط على حد سواء حتى عندما يتهي الأتصال المبتداء بواسطة الخادم ، المجازفة من هذا المخطط هو بأن الخادم قد لايتعاون cooperate ويغلق نهايتة من الإتصال عندما يستلم EOF ، ومع ذلك فأكثر الخوادم تعمل في هذة الناحية in this respect ، فأذا صادفت واحد التي ليست من ذلك يمكنك قتل kill كلا الأب والطفل بضغط على مفتاح الإعتراض .

وهناك ناحية واحدة دقيقة لذلك المخطط ، فلايمكن لعملية الأب من إغلاق close() نسختة من المقبس لكي يرسل الEOF إلى المضيف البعيد ، وهناك نسخة ثانية من المقبس في عملية الطفل ولان يغلق نظام التشغيل filehandle حتى تغلق نسختة الاخيرة ، والحل هو أن الأب يستدعى shutdown(1) على المقبس لإنجبر إغلاق مايكتبة ، وبذلك يرسل EOF إلى الخادم بدون تداخل من مقدرة المقبس إلى الأستمرار لقراءة البيانات القادم في الإتجاة الاخر ، وهذة الإستراتيجية مطبقة في المثال gab2.pl التالي :

#!/usr/bin/perl
# file: gab2.pl
# Figure 5.8: A working implementation of a gab client

# usage: gab2.pl [host] [port]
# Forking TCP network client

use strict;
use IO::Socket qw(:DEFAULT :crlf);

my $host = shift or die "Usage: gab2.pl host [port]\n";
my $port = shift || 'echo';

my $socket = IO::Socket::INET->new("$host:$port") or die $@;

my $child = fork();
die "Can't fork: $!" unless  defined $child;

if ($child) {
  $SIG{CHLD} = sub { exit 0 };
  user_to_host($socket);
  $socket->shutdown(1);
  sleep;

} else {
  host_to_user($socket);
  warn "Connection closed by foreign host.\n";
}

sub user_to_host {
  my $s = shift;
  while (<>) {
    chomp;
    print $s $_,CRLF;
  }
}

sub host_to_user {
  my $s = shift;
  $/ = CRLF;
  while (<$s>) {
    chomp;
    print $_,"\n";
  }
}

كما في السابق شحن الموديل وجلب المضيف والمنفذ من سطر الاوامر ، ثم إنشاء المقبس المتصل .

ثم إستدعاء fork() وتخزين العائد في المتغير $child ، فكما ذكرنا في حالة النجاح ستقوم بتوليد نسخة مطابقة من العملية الحالية ،في عملية الأب تعيد الدالة fork() معرف عملية PID الطفل ، وفي عملية الطفل تعيد fork() العدد صفر ، في الخطأ تعيد غير معرف undef نفحص ذلك ونخرج مع رسالة الخطأ .

عملية الأب لأضافة بيانات من الدخل القياسي إلى المقبس : بقية السكربت مقسم الى نصفين ، النصف الاول لعملية الأب المسؤل عن قراءة السطور من الدخل القياسي وكتابتة إلى الخادم ، والنصف الاخر للطفل المسؤل لقراءة السطور من الخادم وكتابتهم إلى الخرج القياسي . في عملية الأب يوضع المتغير $child (ليس بقيمة الصفر) على الاختبار في بلوك الشرط if في داخل البلوك تنزيل قائد الإشارة signal handler لإشارة CHLD ، في ذلك القائد يستدعى exit() عندما يستلم الإشارة من المستخدم ،ومن ثم إستدعاء الدالة الخاصة بنا user_to_host() التي تنسخ بيانات المستخدم من الدخل القياسي إلى المقبس ، عندما نغلق الدخل القياسي تعاد user_to_host() الى البلوك ، ثم نستدعى shutdown() لإغلاق كتابة المقبس ، ثم النوم sleep بشكل غير محدود حالياً في إنتظار إشارة CHLD متوقعة لإنهاء العملية .

عملية الطفل تنسخ البيانات من المقبس إلى الخرج القياسي بأستدعى الروتين host_to_user() وتعيد تلك الدالة الى بلوك else عندما يغلق المضيف البعيد المقبس ، في ذلك الوقت لانضيف اي شيء سواء تحذير warn المشار بحالة الأتصال بإغلاق المضيف البعيد الإتصال والخروج من السكربت ويولد نظام التشغيل رسالة CHLD بخروج الطفل .

في جزء الروتين user_to_host() مسؤلة عن إضافة البيانات من الدخل القياسي الى المقبس ، وعلى الحلقة قراءة سطر من الدخل القياسي وحذف السطر الجديد ومن ثم الطباعة إلى المقبس بتذيل CRLF إلى النهاية ، وتعاد عندما نغلق الدخل القياسي .

في جزء الروتين host_to_user() صورة مطابقة لسابقة ، بإختلاف فقط بأننا وضعنا المتغير $/ لفصل سجل الدخل إلى CRLF قبل القراءة من المقبس ، لاحظ بأنة ليس هناك سبب من جعل $/ محلي في هذة الحالة لان التغيرات في عملية الطفل لان تؤثر على الأب ، وعندما قراءة السطر الاخير من المقبس تعاد .



قد تتسائل لماذا الأب يذهب الى النوم sleep بدلاً من الخروج exit بعد إغلاق shutdown() نسختة من المقبس ، والأجابة على ذلك ببساطة فحالما يخرج الأب سيرى المستخدم محث سطر الاوامر يتكرار مرة اخرى reappear ، ومع ذلك لايزال الطفل يقراء في هذا الوقت من المقبس ويكتب إلى الخرج القياسي ، وسيختلط خرج الطفل على نحو غير مرغوب مهما المستخدم يشتغل في سطر الاوامر ، وبالنوم sleeping حتى يخرج الطفل يتفادئ الأب هذا السلوك . وقد تتسائل ايضاً حول إستدعاء exit() في قائد إشارة CHLD بينما هذة مشكلة على أرصفة الونيدوز لانها تسبب في التحطم عادتاً ، والحقيقة المرة من ذلك بأن منفذ الونيدوز للبيرل لاتولد أو يستلام إشارات CHLD عندما تموت عملية الطفل ، ولذلك القضية موضوع نقاش ، ولإنهاء سكربت gab2.pl في الونيدوز نضغط على مفتاح الإعتراض .

عندما نحاول للإتصال الى خادم FTP بإستخدام السكربت تظهر النتائج مقنعة اكثر بكثير من السابق وتعرض النتائج Multiline حالياً بشكل صحيح ، وليس هنالك مشكلة في التزامن أو الإنتضار الدائم synchronization or deadlocking .


Output

% gab2.pl phage.cshl.org ftp
220 phage.cshl.org FTP server ready.
USER anonymous
331 Guest login ok, send your complete e-mail address as password.
PASS ok@better.now
230 Guest login ok, access restrictions apply.
HELP
214-The following commands are recognized (* =>'s unimplemented).
   USER     PORT    STO     RMSAM*   RNTO   NLST    MKD     CDUP
   PASS     PASV    APP     EMRSQ*   ABOR   SITE    XMKD    XCUP
   ACCT*    TYPE    MLFL*   MRCP*    DELE   SYST    RMD     STOU
   SMNT*    STRU    MAIL*   ALLO     CWD    STAT    XRMD    SIZE
   REIN*    MODE    MSND*   REST     XCWD   HELP    PWD     MDTM
   QUIT     RETR    MSOM*   RNFR     LIST   NOOP    XPWD    
214 Direct comments to ftp-bugs@phage.cshl.org
QUIT
221 Goodbye.
Connection closed by foreign host.

هذا العميل مناسب لمحدثة العديد من الأسطر الموجهه للخادم ، ولكن هناك خدمة أنترنت واحدة لاتستطيع التعامل مع هذا العميل للوصول بنجاح ،وهي خدمة التلنت لدخول عن بعد remote login، وذلك بسبب ان خوادم التلنت Telnet servers تبداء أولاً تبادل بعض المعلومات بروتوكول الثنائي قبل البدء في المحادثة ، فأذا حولنا لإستخدام هذا العميل لإتصال الى منفذ التلنت (port 23) سنشاهد بعض الحروف المضحة ومن ثم يمهلنا الخادم في الإنتظار الى ان تكتمل مصافحة البروتوكول protocol handshake ، تزود الموديل Net::Telnet طريقة لتحدث إلى خوادم التلنت سناقشها فيما بعد في الفصل القادم .